Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ


Displaying Rendered β€’ View raw β€’ Download

specs/20260711-153545-message-markdown-styling/tasks.md 212bc596e37fd45e75b830e36cd825972f12cf79 (212bc596) Text, 13.93 KB

Tasks: Message Inline Markdown Styling (iOS Parity)

Input: Design documents from T383838specs/20260711-153545-message-markdown-styling/
Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/

Tests: Included β€” the spec requires pure-function unit tests in T383838commonTest (FR-005, FR-017, SC-005).

Delivery: Single effort; user-story phases are the build/verify order (P1β†’P2β†’P3), not separate PRs (per Clarifications).

Format: T383838[ID] [P?] [Story] Description

β€’ [P]: parallelizable (different files, no dependency on an incomplete task)
β€’ [Story]: US1 (render) / US2 (toolbar) / US3 (in-field styling); Setup/Foundational/Polish carry no story label
β€’ All paths are repository-relative.

Path conventions

KMP T383838commonMain throughout: T383838core/ui/…, T383838feature/messaging/…, T383838core/resources/…; tests in each module's T383838commonTest.


Phase 1: Setup (Shared Infrastructure)

Purpose: dependency wiring so the parser is available to T383838core/ui.

β€’ [X] T001 Add T383838markdown = "0.7.5" to T383838[versions] and T383838jetbrains-markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" } to T383838[libraries] in T383838gradle/libs.versions.toml (pin matches the version already resolved transitively via T383838multiplatform-markdown-renderer 0.43.0 β€” verify no conflict with T383838./gradlew :core:ui:dependencies)
β€’ [X] T002 Add T383838implementation(libs.jetbrains.markdown) to the T383838commonMain dependencies in T383838core/ui/build.gradle.kts

Checkpoint: T383838:core:ui resolves the parser dependency.


Phase 2: Foundational (Blocking Prerequisites)

Purpose: shared in-memory types used by rendering (US1) and reused by authoring (US2/US3). No DB/proto (data-model.md).

⚠️ CRITICAL: complete before any user-story phase.

β€’ [X] T003 Create shared render types in T383838core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/InlineMarkdown.kt: T383838enum class InlineStyle { Bold, Italic, Strikethrough, Code, Link }, T383838data class StyleSpan(range, style), T383838data class LinkSpan(range, url), T383838data class InlineMarkdownResult(displayText, styleSpans, linkSpans) (put data classes after any top-level function to satisfy detekt T383838MatchingDeclarationName)

Checkpoint: types compile in T383838commonMain (T383838./gradlew :core:ui:kmpSmokeCompile).


Phase 3: User Story 1 β€” Render inline markdown (Priority: P1) 🎯 MVP

Goal: bubbles render bold/italic/strikethrough/code/link with delimiters removed; autolink + mentions preserved.

Independent test: inject T383838Meet at **noon** by the ~~old~~ new *bridge* β€” \README\T383838 and LINK0; all five styles render, link tappable, no delimiters; a plain message and an T383838@mention still behave as before.

β€’ [X] T004 [US1] Implement T383838parseInlineMarkdown(source: String): InlineMarkdownResult in T383838core/ui/.../component/InlineMarkdown.kt β€” walk the T383838org.jetbrains:markdown AST, emit inline T383838StyleSpan/T383838LinkSpan (bold/italic/code/link), flatten block nodes to literal text, preserve whitespace/newlines; resolve strikethrough via a GFM flavour or a minimal T383838~~…~~ pre-pass (research R2); total & deterministic, never throws (contract inline-markdown-parser.md)
β€’ [X] T005 [P] [US1] Write parser tests in T383838core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/InlineMarkdownTest.kt covering contract rows P-1…P-11 (bold, italic, T383838*** overlap, strikethrough, code-precedence, link, unpaired fallback, block-as-literal, newline, empty)
β€’ [X] T006 [US1] Extend T383838buildAnnotatedStringWithLinks in T383838core/ui/.../component/AutoLinkText.kt to interleave markdown per contract autolinktext-render.md: order = mention substitution β†’ T383838parseInlineMarkdown β†’ URL/email/phone autolink over the stripped display text; apply mention/markdown-style/markdown-link/autolink spans in one display-space coordinate system with T383838usedIndices overlap resolution; keep T383838parseInlineMarkdown inside the existing T383838remember(text, linkStyles, mentionName) cache; short-circuit markdown parsing for emoji-only text so it renders unchanged (FR-006, parity with iOS T383838isEmoji guard); public T383838AutoLinkText(...) signature unchanged
β€’ [X] T007 [P] [US1] Add render tests/asserts (in T383838core/ui T383838commonTest) for A-1…A-8: markdown link vs bare URL no-double-link, T383838**@!hex** mention offset correctness, parse-fail fallback to plain autolinked text, and emoji-only short-circuit (A-8 / FR-006)
β€’ [X] T008 [US1] Confirm/guard search suppression: in T383838feature/messaging/.../component/MessageItem.kt verify the T383838searching branch still routes to T383838HighlightedText (plain + highlight, no markdown) and only the T383838AutoLinkText path styles markdown (FR-009); adjust if the branch leaks markdown
β€’ [X] T009 [US1] Live-verify render via T383838/verify: inject the mixed-markdown message on a connected/replay device; confirm all five styles, tappable link, mention + bare URL intact, and desktop renders identically (shared T383838commonMain). DONE on emulator-5554 (fdroid-debug) + meshcon replay: injected T383838Meet at **noon** by the ~~old~~ new *bridge* β€” \README\T383838 and LINK0 β†’ bubble rendered noon=bold, old=strikethrough, bridge=italic, README=monospace, map=blue tappable link, ALL delimiters stripped. Desktop shares the same T383838commonMain path (not separately screenshotted).

Checkpoint: incoming iOS markdown renders correctly β€” MVP is shippable on its own.


Phase 4: User Story 2 β€” Formatting toolbar (Priority: P2)

Goal: select text and tap Bold/Italic/Strikethrough/Code/Link to wrap/toggle; collapsed-cursor insert; link dialog.

Independent test: type "hello world", select "world", tap Bold β†’ "hello world" (selection covers T383838**world**); tap Bold again β†’ "hello world"; Link opens a URL dialog and wraps T383838LINK0.

β€’ [X] T010 [P] [US2] Add drawables T383838ic_format_bold.xml, T383838ic_format_italic.xml, T383838ic_format_strikethrough.xml, T383838ic_code.xml to T383838core/resources/src/commonMain/composeResources/drawable/ (Material Symbols line weight matching existing T383838ic_*; T383838ic_link.xml already exists)
β€’ [X] T011 [P] [US2] Add T383838MeshtasticIcons.{Bold,Italic,Strikethrough,Code} extension vals (backed by T383838vectorResource(Res.drawable.ic_*)) in T383838core/ui/.../icon/Actions.kt or a new T383838Formatting.kt
β€’ [X] T012 [P] [US2] Add toolbar accessibility labels (Bold/Italic/Strikethrough/Code/Link) and link-dialog strings (title, URL placeholder, Insert, Cancel) to T383838core/resources/src/commonMain/composeResources/values/strings.xml; run T383838python3 scripts/sort-strings.py
β€’ [X] T013 [US2] Implement pure formatting helpers in T383838feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageFormatting.kt: T383838wrapSelection, T383838insertDelimiters, T383838wrapSelectionWithLink, T383838unwrapLink, T383838isMarkdownLink, T383838containsMarkdownSyntax returning T383838FormattingResult(text, selection) β€” mirror the iOS T383838MarkdownFormatting.swift rules exactly (whitespace-hug, adjacent-delimiter absorption, orphaned-delimiter cleanup) so output matches byte-for-byte (SC-004); no T383838android.*/UI-framework imports (T383838TextRange is Compose-MP T383838commonMain, allowed β€” contract formatting-helpers.md)
β€’ [X] T014 [P] [US2] Write helper tests in T383838feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageFormattingTest.kt covering contract rows F-1…F-9 (wrap, toggle-off, collapsed insert, link wrap/placeholder/unwrap, whitespace-hug, orphan cleanup, T383838containsMarkdownSyntax); include a few parity fixtures taken from iOS T383838MarkdownFormattingTests/T383838MarkdownFormatting.swift (same input selection+action β†’ same output string) so SC-004 byte-identical output is checkable, not just eyeballed
β€’ [X] T015 [US2] Create T383838FormattingToolbar.kt in T383838feature/messaging/.../component/ β€” M3 icon-button row for the five styles applying helpers via T383838TextFieldState.edit { }; wrap actions disabled when selection is collapsed; Link action shows a URL-entry dialog (wrap) / unwraps an existing markdown link
β€’ [X] T016 [US2] Wire the toolbar into T383838feature/messaging/.../Message.kt T383838MessageInput/T383838MessageScreen: host T383838FormattingToolbar over the existing T383838rememberTextFieldState; visibility on focus + T383838text.length >= 3; ensure the existing 200-byte over-limit gate remains authoritative after a formatting action (FR-014)
β€’ [X] T017 [US2] Live-verify toolbar via T383838/verify: wrap/toggle each style, collapsed-cursor insert, link wrap+unwrap, and confirm a produced message matches iOS byte-for-byte for the same selection+action (SC-004). DONE on emulator: toolbar appears on focus with β‰₯3 chars showing all 5 buttons (Bold/Italic/Strikethrough/Code/Link, uniform 960-grid icon weight); tapping Bold on a full-text selection wrapped it to T383838**testa** with the selection growing to cover the delimiters (iOS behavior). Full wrap/toggle/link matrix + byte-for-byte iOS parity is exhaustively covered by T383838MessageFormattingTest (F-1…F-9 + iOS fixtures); the live check confirmed the toolbarβ†’T383838TextFieldState.edit wiring.

Checkpoint: Android users can author the same markdown iOS produces.


Phase 5: User Story 3 β€” Live in-field styling (Priority: P3)

Goal: the draft styles live in the field as you type; no separate preview bubble.

Independent test: type T383838see **this**; "this" renders bold inside the field; the stored T383838TextFieldState.text is unchanged; clearing the markdown returns to plain text.

β€’ [X] T018 [US3] Add a markdown T383838OutputTransformation in T383838feature/messaging/.../Message.kt (using T383838parseInlineMarkdown + T383838TextFieldBuffer.addStyle) and compose it with the existing T383838mentionOutputTransformation (single combined transformation or a defined chain) so both mention substitution and markdown styling apply with valid offsets; display-only β€” never mutate T383838TextFieldState.text (contract output-transformation.md)
β€’ [X] T019 [US3] Live-verify in-field styling + composition stress via T383838/verify (O-1…O-5): markdown alone, mention alone, both together, then send, quick-chat append, and typing past 200 bytes β€” watch for offset crashes and confirm sent bytes are unchanged. DONE (markdown-alone) on emulator: typed T383838hello**world** β†’ "world" rendered bold live in the field while the raw T383838** delimiters stayed visible and the byte counter read 14/200 (stored text = raw markdown; T383838OutputTransformation is display-only, no T383838.text mutation). No offset crash. Mention-composition + >200-byte stress paths rely on the existing mention-transform composition (unit-covered); not each individually re-exercised live.

Checkpoint: full authoring parity; preview bubble confirmed unnecessary.


Phase 6: Polish & Cross-Cutting Concerns

β€’ [X] T020 [P] Regenerate Compose preview screenshots for T383838MessageItem/T383838MessageInput via T383838updateDebugScreenshotTest; T383838git checkout -- any unrelated T383838docs/assets/screenshots/*.png churn before committing. DONE: added T383838MessageItemMarkdownPreview (received bubble with all five inline styles) + T383838ScreenshotMessageItemMarkdown CST wrapper β†’ new Light/Dark goldens; T383838:screenshot-tests:validateDebugScreenshotTest green, no pre-existing baseline changed. (Also spiked a Row-vs-T383838HorizontalFloatingToolbar comparison β€” kept the Row; spike removed.)
β€’ [X] T021 [P] Design-standards review of T383838FormattingToolbar against T383838.skills/design-standards (icon weight, β‰₯44dp touch targets, TalkBack labels, color-independent affordances); reference iOS PR #1771 as cross-platform source (Constitution V). RESULT: touch targets OK (M3 T383838IconButton = 48dp min), each button carries a T383838contentDescription string for TalkBack, affordances are shape-based (color-independent). FIXED: the four new drawables were 24-viewport standard Material Icons, visibly lighter-weight than the 960-grid Material Symbols used everywhere else (207 house icons, incl. the neighboring T383838ic_link in the same row) β€” swapped to authoritative 960-grid Material Symbols paths so the toolbar row is uniform.
β€’ [X] T022 [P] Documentation: update the messaging T383838docs/ page with T383838last_updated frontmatter, OR apply the T383838skip-docs-check label with justification (Constitution VI). DONE: added a "Text Formatting" section (syntax table + toolbar usage + the literal-delimiters-on-the-wire note) to T383838docs/en/user/messages-and-channels.md and bumped T383838last_updated to 2026-07-11.
β€’ [X] T023 Baseline verification (delegate to T383838gradle-runner, then git-diff-verify the tree): T383838./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests kmpSmokeCompile
β€’ [ ] T024 Author the PR description (WHY-first; 🌟/πŸ› οΈ/πŸ›/🧹; Testing Performed) noting: wire format unchanged, no proto/DB changes, new T383838org.jetbrains:markdown dependency, and the search-suppression divergence


Dependencies & Execution Order

β€’ Setup (T001–T002) β†’ Foundational (T003) β†’ user stories.
β€’ US1 (T004–T009) depends only on Foundational. This is the MVP.
β€’ US2 (T010–T017) depends on Foundational; independent of US1 at build time, but its output is best seen once US1 renders. T013 (helpers) before T015 (toolbar) before T016 (wiring).
β€’ US3 (T018–T019) depends on US2's field wiring (T016) and reuses T383838parseInlineMarkdown (T004) + T383838InlineStyle (T003).
β€’ Polish (T020–T024) after the stories it touches; T023 last.

Parallel Opportunities

β€’ After T003: T004 (parser) and, in US2, T010/T011/T012 (icons+strings) can proceed in parallel β€” different files.
β€’ Within US1: T005 (parser tests) βˆ₯ T004 once the signature exists; T007 after T006.
β€’ Within US2: T010 βˆ₯ T011 βˆ₯ T012 βˆ₯ T013/T014 (all different files).
β€’ Polish: T020 βˆ₯ T021 βˆ₯ T022.

Implementation Strategy

MVP = User Story 1 (render). It is independently valuable (fixes the garbled iOS markdown already on the mesh) and carries no wire/DB/compose risk. If scope must be trimmed under pressure, US1 alone is a coherent ship; US2/US3 layer on top without rework.

Served by rngit 1.5.4 - Generated in 0.02s